# -*- coding: utf-8 -*-
# @Time    : 2019/10/28 19:13
# @Author  : LiHaowen
# @Email   : lihaowen2017@163.com
# @File    : select_http.py

import socket
from urllib.parse import urlparse
# DefaultSelector是封装select的提供注册方法
from selectors import DefaultSelector, EVENT_WRITE, EVENT_READ


# 可以选择io复用的方法，提供注册机制
# select + 回调 + 事件循环
# 并发性高
# 使用单线程，不存在线程切换的开销

selector = DefaultSelector()
# 使用select完成http请求
urls = []
stop = False


class Fetcher:

    def connected(self, key):
        # 回调函数， 当监听到写事件后需要的操作
        # 回调函数中需要注销
        selector.unregister(key.fd)
        # 不再需要try pass，因为启用了事件监听，已经是数据就绪的状态
        self.client.send("GET {} HTTP/1.1\r\nHost:{}\r\nConnection:close\r\n\r\n".format(self.path, self.host).encode("utf8"))
        # 注册 监听是否是可写的状态
        selector.register(self.client.fileno(), EVENT_READ, self.readable)

    def readable(self, key):
        # data = b""
        d = self.client.recv(1024)
        if d:
            self.data += d
        else:
            selector.unregister(key.fd)
            data = self.data.decode("utf8")
            html_data = data.split("\r\n\r\n")[1]
            print(data)
            print(html_data)
            self.client.close()
            # urls中一个url处理完成就移除url
            urls.remove(self.spider_url)
            # 判断urls为空
            if not urls:
                global stop
                stop = True

    def get_url(self, url):
        self.spider_url = url
        url = urlparse(url)
        self.host = url.netloc
        self.data = b""
        self.path = url.path
        if self.path == "":
            self.path = "/"
        # 建立socket连接 进行http请求
        self.client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.client.setblocking(False)  # 立即返回 非阻塞式I/O
        try:
            self.client.connect((self.host, 80))  # 连接 阻塞不会消耗cpu
        except BlockingIOError as e:
            pass

        # 注册 监听是否是可写的状态
        # client.fileno() 文件描述符, 监听状态，回调函数名
        selector.register(self.client.fileno(), EVENT_WRITE, self.connected)


def loop():
    # 事件循环，不停的请求socket的状态来调用对应的回调函数
    # 1. select本身是不支持register模式 selector对select进行了封装使其支持register
    # 拿到某一socket句柄时就可以知道该使用哪个回调方法
    # 2. socket状态变化后的回调是由程序员完成的
    # 所以要写事件循环函数
    while not stop:
        # windows 下默认是select，select接收到空列表时会报错
        # linux下并不会报错
        ready = selector.select()  # 返回值是一个列表，包含了元组key包含一个nametuple，为一个选择键对象，将文件描述符等于key进行关联

        for key, mask in ready:  # 拆包
            call_back = key.data  # 找到回调函数
            call_back(key)   # key的fd是client.fileno()返回值socket句柄
    # 回调+事件循环+select/poll/epoll


if __name__ == '__main__':
    fetcher = Fetcher()
    # fetcher.get_url("https://www.baidu.com")
    # 并发性能测验
    import time
    start_time = time.time()
    for url in range(20):
        url = f"http://jsonplaceholder.typicode.com/posts/{url}"
        urls.append(url)
        fetcher = Fetcher()
        fetcher.get_url(url)
    loop()
    print("finnal time", time.time() - start_time)

